package ru.myrtle.umf.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Locale;

import ru.myrtle.umf.UAttribute;
import ru.myrtle.umf.UClass;
import ru.myrtle.umf.UClassifier;
import ru.myrtle.umf.UDataType;
import ru.myrtle.umf.UElement;
import ru.myrtle.umf.UEnumeration;
import ru.myrtle.umf.UEnumerationLiteral;
import ru.myrtle.umf.UFeature;
import ru.myrtle.umf.UObject;
import ru.myrtle.umf.UOperation;
import ru.myrtle.umf.UParameter;
import ru.myrtle.umf.UReference;

public class UUtilities {

    private static class UnmodifiableIterator<T> implements Iterator<T> {

        private final Iterator<T> data;

        private UnmodifiableIterator(Iterator<T> data) {
            this.data = data;
        }

        @Override
        public boolean hasNext() {
            return data.hasNext();
        }

        @Override
        public T next() {
            return data.next();
        }

        @Override
        public void remove() {
            throw unsupported();
        }
    }

    /*
     *
     */

    private static class UnmodifiableListIterator<T> implements ListIterator<T> {

        private final ListIterator<T> data;

        private UnmodifiableListIterator(ListIterator<T> data) {
            this.data = data;
        }

        @Override
        public boolean hasNext() {
            return data.hasNext();
        }

        @Override
        public T next() {
            return data.next();
        }

        @Override
        public boolean hasPrevious() {
            return data.hasPrevious();
        }

        @Override
        public T previous() {
            return data.previous();
        }

        @Override
        public int nextIndex() {
            return data.nextIndex();
        }

        @Override
        public int previousIndex() {
            return data.previousIndex();
        }

        @Override
        public void remove() {
            throw unsupported();
        }

        @Override
        public void set(T t) {
            throw unsupported();
        }

        @Override
        public void add(T t) {
            throw unsupported();
        }
    }

    /*
     *
     */

    private static class UnmodifiableUListImpl<T> implements UList<T> {

        private final UList<T> data;

        private UnmodifiableUListImpl(UList<T> data) {
            this.data = data;
        }

        @Override
        public int size() {
            return data.size();
        }

        @Override
        public boolean isEmpty() {
            return data.isEmpty();
        }

        @Override
        public boolean contains(Object o) {
            return data.contains(o);
        }

        @Override
        public Iterator<T> iterator() {
            return new UnmodifiableIterator<T>(
                    data.iterator()
            );
        }

        @Override
        public Object[] toArray() {
            return data.toArray();
        }

        @Override
        public <T> T[] toArray(T[] a) {
            return data.toArray(a);
        }

        @Override
        public boolean add(T t) {
            throw unsupported();
        }

        @Override
        public boolean remove(Object o) {
            throw unsupported();
        }

        @Override
        public boolean containsAll(Collection<?> c) {
            return data.containsAll(c);
        }

        @Override
        public boolean addAll(Collection<? extends T> c) {
            throw unsupported();
        }

        @Override
        public boolean addAll(int index, Collection<? extends T> c) {
            throw unsupported();
        }

        @Override
        public boolean removeAll(Collection<?> c) {
            throw unsupported();
        }

        @Override
        public boolean retainAll(Collection<?> c) {
            throw unsupported();
        }

        @Override
        public void clear() {
            throw unsupported();
        }

        @Override
        public T get(int index) {
            return data.get(index);
        }

        @Override
        public T set(int index, T element) {
            throw unsupported();
        }

        @Override
        public void add(int index, T element) {
            throw unsupported();
        }

        @Override
        public T remove(int index) {
            throw unsupported();
        }

        @Override
        public int indexOf(Object o) {
            return data.indexOf(o);
        }

        @Override
        public int lastIndexOf(Object o) {
            return data.lastIndexOf(o);
        }

        @Override
        public ListIterator<T> listIterator() {
            return new UnmodifiableListIterator<T>(
                    data.listIterator()
            );
        }

        @Override
        public ListIterator<T> listIterator(int index) {
            return new UnmodifiableListIterator<T>(
                    data.listIterator(index)
            );
        }

        @Override
        public UList<T> subList(int fromIndex, int toIndex) {
            return new UnmodifiableUListImpl<T>(
                    data.subList(fromIndex, toIndex)
            );
        }
    }

    /*
     *
     */

    private static class SafeSimpleDateFormat extends SimpleDateFormat {

        private static final long serialVersionUID = 1L;

        public SafeSimpleDateFormat(String pattern) {
            super(pattern, Locale.ENGLISH);
        }

        @Override
        public synchronized Date parse(String source) throws ParseException {
            return super.parse(source);
        }

        @Override
        public synchronized StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
            return super.format(date, toAppendTo, fieldPosition);
        }
    }

    protected static final DateFormat[] DATE_FORMATS = {
            new SafeSimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.'SSSZ"),
            new SafeSimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.'SSS"),
            new SafeSimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
            new SafeSimpleDateFormat("yyyy-MM-dd'T'HH:mm"),
            new SafeSimpleDateFormat("yyyy-MM-dd")
    };

    /*
     *
     */

    private static UnsupportedOperationException unsupported() {
        return new UnsupportedOperationException("Modification not allowed");
    }

    /*
     *
     */

    public static <T> UList<T> unmodifiableUList(UList<T> source) {
        return new UnmodifiableUListImpl<T>(source);
    }

    public static <T> T selectFirst(UList<T> source, UPredicate<T> criteria) {
        for (T object : source) {
            if (criteria.evaluate(object)) {
                return object;
            }
        }

        return null;
    }

    public static <T> void select(UList<T> target, UList<T> source, UPredicate<T> criteria) {
        for (T object : source) {
            if (criteria.evaluate(object)) {
                target.add(object);
            }
        }
    }

    public static <S, T> void select(UList<T> target, UList<S> source, UPredicate<S> criteria, UTransformer<S, T> transformer) {
        for (S object : source) {
            if (criteria.evaluate(object)) {
                target.add(
                        transformer.transform(object)
                );
            }
        }
    }

    /*
     *
     */

    /**
     * Returns the wrapper class for the primitive class, or the original class,
     * if it's not a primitive class.
     *
     * @param javaClass
     */
    public static Class<?> wrapperClassFor(Class<?> javaClass) {
        if (javaClass == null) {
            return null;
        }
        else if (javaClass.isPrimitive()) {
            if (javaClass == Boolean.TYPE) {
                return Boolean.class;
            }
            else if (javaClass == Integer.TYPE) {
                return Integer.class;
            }
            else if (javaClass == Float.TYPE) {
                return Float.class;
            }
            else if (javaClass == Double.TYPE) {
                return Double.class;
            }
            else if (javaClass == Long.TYPE) {
                return Long.class;
            }
            else if (javaClass == Short.TYPE) {
                return Short.class;
            }
            else if (javaClass == Byte.TYPE) {
                return Byte.class;
            }
            else // if (javaClass == Character.TYPE)
            {
                return Character.class;
            }
        }
        else {
            return javaClass;
        }
    }

    public static String convertToString(UDataType uDataType, Object objectValue) {
        if (objectValue == null) {
            return null;
        }
        else if (objectValue instanceof Character) {
            Integer value = (int) (Character) objectValue;
            return value.toString();
        }
        else if (objectValue.getClass() == Date.class) {
            return DATE_FORMATS[0].format((Date) objectValue);
        }
        else {
            return objectValue.toString();
        }
    }

    public static Object convertFromString(UDataType uDataType, String stringValue) {
        if (stringValue == null) {
            return null;
        }

        if (uDataType instanceof UEnumeration) {
            Object result = ((UEnumeration) uDataType).getLiteralByName(stringValue);

            if (result == null) {
                throw new IllegalArgumentException(
                        "The value '" + stringValue + "' is not a valid enumerator of '" + uDataType.getName() + "'"
                );
            }

            return result;
        }

        Class<?> c = wrapperClassFor(
                uDataType.getInstanceClass()
        );

        if (c == null) {
            return null;
        }

        if (c == Character.class) {
            char charValue = 0;
            try {
                Integer value = new Integer(stringValue);
                charValue = (char) value.intValue();
            }
            catch (NumberFormatException e) {
                char[] carray = stringValue.toCharArray();
                charValue = carray[0];
            }

            return new Character(charValue);
        }

        if (c == Date.class) {
            for (DateFormat DATE_FORMAT : DATE_FORMATS) {
                try {
                    return DATE_FORMAT.parse(stringValue);
                }
                catch (ParseException parseException) {
                    // Keep trying until all else has failed.
                }
            }

            throw new IllegalArgumentException(
                    "The value '" + stringValue + "' is not a date formatted string of the form yyyy-MM-dd'T'HH:mm:ss'.'SSSZ or a valid subset thereof"
            );
        }

        Class<String> stringClass = String.class;
        Class<?>[] signature = { stringClass };

        Constructor<?> ctor = null;
        try {
            ctor = c.getConstructor(signature);
        }
        catch (NoSuchMethodException e) {
            // Continue to try a different approach.
        }

        Throwable formatException = null;
        try {
            if (ctor != null) {
                return ctor.newInstance(stringValue);
            }
        }
        catch (InstantiationException e) {
            formatException = e;
        }
        catch (InvocationTargetException e) {
            formatException = e.getCause();
        }
        catch (IllegalAccessException e) {
            formatException = e;
        }

        Method valueOf = null;
        try {
            valueOf = c.getMethod("valueOf", signature);
        }
        catch (NoSuchMethodException e) {
            // Continue to try a different approach.
        }

        try {
            if (valueOf != null) {
                return valueOf.invoke(null, stringValue);
            }
        }
        catch (IllegalArgumentException e) {
            formatException = e;
        }
        catch (InvocationTargetException e) {
            formatException = e.getCause();
        }
        catch (IllegalAccessException e) {
            formatException = e;
        }

        String exceptionString = formatException != null ? formatException.toString() : "";

        throw new IllegalArgumentException(
                "The value '" + stringValue + "' is invalid. " + exceptionString, formatException
        );
    }

    /*
     *
     */

    public static void attach(UObject container, UReference containingReference, UObject containedObject) {
        if (containingReference.isMany()) {
            @SuppressWarnings(
                    "unchecked"
            )
            UList<Object> values = (UList<Object>) container.uGet(containingReference);

            values.add(
                    containedObject
            );
        }
        else {
            container.uSet(
                    containingReference, containedObject
            );
        }
    }

    public static void detach(UObject container, UReference containingReference, UObject containedObject) {
        if (containingReference.isMany()) {
            @SuppressWarnings(
                    "unchecked"
            )
            UList<Object> values = (UList<Object>) container.uGet(containingReference);

            values.remove(
                    containedObject
            );
        }
        else {
            container.uUnset(
                    containingReference
            );
        }
    }

    /*
     *
     */

    public static void initElement(UElement uElement, String name) {
        uElement.setName(name);
    }

    public static void initClassifier(UClassifier uClassifier, String name, Class instanceClass) {
        initElement(
                uClassifier, name
        );

        uClassifier.setInstanceClass(instanceClass);
    }

    public static void initDataType(UDataType uDataType, String name, Class instanceType, UFlags.Serializable isSerializable, UConverter converter) {
        initClassifier(
                uDataType, name, instanceType
        );

        uDataType.setSerializable(isSerializable.flag());
        uDataType.setConverter(converter);
    }

    public static void initEnumeration(UEnumeration uEnumeration, String name, Class instanceType, UFlags.Serializable isSerializable) {
        UConverter converter = new UEnumerationConverter(
                uEnumeration
        );

        initDataType(
                uEnumeration, name, instanceType, isSerializable, converter
        );
    }

    public static void initEnumerationLiteral(UEnumerationLiteral uEnumerationLiteral, String name) {
        initElement(
                uEnumerationLiteral, name
        );
    }

    public static void initClass(UClass uClass, String name, Class<?> instanceType, UFlags.Provided provided, UFlags.Virtual virtual, UClass... superTypes) {
        initClassifier(
                uClass, name, instanceType
        );

        uClass.setProvided(provided.flag());
        uClass.setVirtual(virtual.flag());
        uClass.getSuperTypes().addAll(
                Arrays.asList(
                        superTypes
                )
        );
    }

    public static void initFeature(UFeature uFeature, String name, UFlags.Transient isTransient, UFlags.Volatile isVolatile, UFlags.Changeable isChangeable, UFlags.Derived isDerived) {
        initElement(
                uFeature, name
        );

        uFeature.setTransient(isTransient.flag());
        uFeature.setVolatile(isVolatile.flag());
        uFeature.setChangeable(isChangeable.flag());
        uFeature.setDerived(isDerived.flag());
    }

    public static void initAttribute(UAttribute uAttribute, String name, UFlags.Transient isTransient, UFlags.Volatile isVolatile, UFlags.Changeable isChangeable, UFlags.Derived isDerived, UDataType type, UFlags.Identifying isIdentifyingAttribute) {
        initFeature(
                uAttribute, name, isTransient, isVolatile, isChangeable, isDerived
        );

        uAttribute.setType(type);
        uAttribute.setIdentifyingAttribute(isIdentifyingAttribute.flag());
    }

    public static void initReference(UReference uReference, String name, UFlags.Transient isTransient, UFlags.Volatile isVolatile, UFlags.Changeable isChangeable, UFlags.Derived isDerived, UClass type, UFlags.Many many, UFlags.Ordered ordered, UFlags.Unique unique, UFlags.Containment containment, UReference opposite) {
        initFeature(
                uReference, name, isTransient, isVolatile, isChangeable, isDerived
        );

        uReference.setType(type);
        uReference.setMany(many.flag());
        uReference.setOrdered(ordered.flag());
        uReference.setUnique(unique.flag());

        uReference.setContainment(containment.flag());
        uReference.setOpposite(opposite);
    }

    public static void initOperation(UOperation uOperation, String name, UClassifier type, UFlags.Provided provided, UFlags.Many many, UFlags.Ordered ordered, UFlags.Unique unique) {
        initElement(
                uOperation, name
        );

        uOperation.setProvided(provided.flag());
        uOperation.setType(type);
        uOperation.setMany(many.flag());
        uOperation.setOrdered(ordered.flag());
        uOperation.setUnique(unique.flag());
    }

    public static void initParameter(UParameter uParameter, String name, UClassifier type, UFlags.Many many, UFlags.Ordered ordered, UFlags.Unique unique) {
        initElement(
                uParameter, name
        );

        uParameter.setType(type);
        uParameter.setMany(many.flag());
        uParameter.setOrdered(ordered.flag());
        uParameter.setUnique(unique.flag());
    }

    /*
     *
     */

    @SuppressWarnings(
            "unchecked"
    )
    public static <T extends UObject> T clone(UObject source) {
        if (source == null) {
            return null;
        }

        UClass klass = source.uClass();
        UObject target = klass.createInstance();

        for (UFeature feature : klass.getAllFeatures()) {
            if (feature instanceof UReference) {
                UReference reference = (UReference) feature;

                if (reference.isContainment()) {
                    if (reference.isMany()) {
                        Collection<UObject> sourceList = (Collection<UObject>) source.uGet(feature);
                        Collection<UObject> targetList = (Collection<UObject>) target.uGet(feature);

                        for (UObject object : sourceList) {
                            targetList.add(
                                    clone(object)
                            );
                        }
                    }
                    else {
                        target.uSet(
                                feature,
                                clone(
                                        (UObject) source.uGet(feature)
                                )
                        );
                    }
                }
                else {
                    target.uSet(
                            feature, source.uGet(feature)
                    );
                }
            }
            else {
                target.uSet(
                        feature, source.uGet(feature)
                );
            }
        }

        return (T) target;
    }
}
